<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>D3 Bar Chart</title>
    <script src="js/d3@7.js"></script>
    <!-- <script src="https://cdn.jsdelivr.net/npm/d3@7"></script> -->
    <style>
        path.link {
            fill: none;
            stroke: #999;
            stroke-width: 1;
        }
    </style>
</head>

<body>
    <div id="svgContainer">
        <div id="body"></div>
    </div>
    <script>

        const greenDelIcon = './public/images/del_green.png';
        const greenAddIcon = './public/images/add_green.png';
        const blueDelIcon = './public/images/del_blue.png';
        const blueAddIcon = './public/images/add_blue.png';
        // let d3 = null;
        //数据源
        var root = {
            "x0": null,
            "y0": null,
            'name': '中国',
            'children': [{
                'name': '浙江',
                "_name": 'zhejiang',
                'children': []
            },
            {
                'name': '广西',
                'children': [],
                "_name": 'guangxi',
            },
            {
                'name': '黑龙江',
                "_name": 'heilongjiang',
            }
            ]
        }
        var utils = {
            // 缩放值
            minSize: 1,
            maxSize: 3,
            // rate: 1.2,
            // 节点间距
            nodeSpacing: [130, 60]
        };
        //布局和尺寸参数
        var margin = {
            top: 20,
            right: 120,
            bottom: 20,
            left: 120
        },
            height = 800 - margin.top - margin.bottom;
        //常规参数

        var i = 0,// 临时id计数
            duration = 750,// 过渡动画时间
            rectW = 120,//矩形宽
            rectH = 60;


        function createTree() {
            var tree = d3.tree().nodeSize(utils.nodeSpacing);//将每个节点的水平和垂直间距设置为 130 和 60 像素。
            var hierarchyData = d3.hierarchy(root)//包含了每个节点的位置 (x 和 y 坐标)
            console.log("🚀 ~ createTree ~ hierarchyData:", hierarchyData)

            //path连线
            function linkVertical(d) {
                // return "M" + d.source.x + "," + d.source.y       //曲线
                //     + "C" + (d.source.x + d.target.x) / 2 + "," + d.source.y
                //     + " " + (d.source.x + d.target.x) / 2 + "," + d.target.y
                //     + " " + d.target.x + "," + d.target.y;
                let sourceX = d.source.x, //折线
                    sourceY = d.source.y + rectH,
                    targetX = d.target.x,
                    targetY = d.target.y;
                // console.log("🚀 ~ linkVertical ~ sourceX:", sourceX, sourceY, targetX, targetY)
                return "M" + sourceX + "," + sourceY +
                    "V" + ((targetY - sourceY) / 2 + sourceY) +
                    "H" + targetX +
                    "V" + targetY;
            }


            //创建一个支持缩放和平移功能的 SVG 画布
            var svg = d3.select("#body").append("svg").attr('width', '1000').attr('height', '1000')
                .call(d3?.zoom().scaleExtent([utils.minSize, utils.maxSize]).on("zoom",
                    function redraw(event) {
                        svg.attr("transform", event.transform);
                    }
                )).append("g").attr("transform", "translate(" + 350 + "," + 20 + ")").append("g");

            (root).x0 = 0;
            (root).y0 = height / 2;

            update(root);

            function update(source) {
                console.log("🚀 ~ update ~ source:", source)
                //使用hierarchy层级布局生成树结构
                let treeData = tree(hierarchyData);
                console.log("🚀 ~ update ~ treeData:", treeData)
                let nodes = treeData.descendants().reverse();
                let links = treeData.links();
                console.log("🚀 ~ update ~ links:", links)
                //设置节点的y坐标来实现节点的垂直间隔
                nodes.forEach(function (d) { d.y = d.depth * 180; });
                console.log("🚀 ~ update ~ nodes:", nodes)

                // Update the nodes…
                var node = svg.selectAll('g.node').data(nodes, (d) => d.id || (d.id = ++i));//选中所有的node类的g元素 获取update部分

                //获取enter部分

                /* 
                 新节点进入时：(线同理)
                 新节点一开始出现在父节点的位置（source.x0 和 source.y0），然后在动画中平滑地移动到它们的实际位置（d.x 和 d.y）。
        
                 节点更新时：
                 已存在的节点会从当前的位置平滑地移动到它们的新位置。
        
                 节点退出时：
                 节点从它们的当前位置（d.x 和 d.y）平滑地移动到父节点的位置（source.x 和 source.y），然后被移除。
                 */
                var nodeEnter = node.enter().append("g").attr("class", "node")
                    .attr("transform", function (d) {
                        return "translate(" + source.x0 + "," + source.y0 + ")";
                    })

                console.log("🚀 ~ update ~ nodeEnter:", nodeEnter)

                nodeEnter.append("rect").attr("width", rectW).attr("height", rectH)
                    .attr("stroke", "#EBEBEB")//设置矩形的边框颜色
                    .attr('y', 0).attr('x', -rectW / 2)
                    .attr("stroke-width", 1)//设置矩形的边框宽度为 1 像素。
                    .style("fill", function (d) {
                        console.log(d._children, "111111", d, d.children,);
                        return d._children ? "#cdff17" : "#ff6c17";
                    })
                /*     .on("click", function (e, node) {
                        click(node)
                    }); */
                nodeEnter.append("text").attr("x", rectW / 2 - rectW / 2).attr("y", rectH / 2)
                    .attr("dy", ".35em").attr("text-anchor", "middle")//一种在 y 方向上偏移文本的方法
                    .text(function (d) {
                        return d.data.name;
                    });

                nodeEnter.append('image').attr('width', 16).attr('height', 16).attr('x', 30).attr('y', 40)
                    .attr('class', 'operation-box')
                    .attr('xlink:href', (d) => {
                        const greenIcon = d.children ? greenDelIcon : greenAddIcon;
                        const blueIcon = d.children ? blueDelIcon : blueAddIcon;
                        return d.depth === 0 ? blueIcon : greenIcon;
                    })
                    .on("click", function (e, node) {
                        // console.log("🚀 ~ e, node:", e, node)
                        click(node)
                    });
                let nodeUpdate = nodeEnter.merge(node);//新进入的节点 + 已经存在的节点合并 == 包含所有节点的选择集
                console.log("🚀 ~ update ~ nodeUpdate:", nodeUpdate)
                nodeUpdate.transition()//为所有节点添加过渡动画，使它们从当前的位置平滑地移动到新的位置。
                    .duration(750)
                    .attr("transform", function (d) {
                        // console.log("🚀 transform ~ d:", d)
                        return "translate(" + d.x + "," + d.y + ")";
                    });

                // 图标更新
                nodeUpdate.select('.operation-box').attr('xlink:href', (d) => {
                    const greenIcon = d.children ? greenDelIcon : greenAddIcon;
                    const blueIcon = d.children ? blueDelIcon : blueAddIcon;
                    // console.log("🚀 nodeUpdate ~ .attr ~ d:", d, d.children, d.depth)
                    return d.depth === 0 ? blueIcon : greenIcon;
                });

                // 新节点在进入时通过 nodeEnter.append("rect") 添加rect元素，之后在更新过程中通过 nodeUpdate.select("rect") 统一更新样式。
                // nodeUpdate.select("rect")
                //     .attr("width", rectW)
                //     .attr("height", rectH)
                //     .attr("stroke", "black")
                //     .attr("stroke-width", 1)
                //     .style("fill", function (d) {
                //         return d._children ? "#cdff17" : "#ff6c17";
                //     });
                nodeUpdate.select("text").style("fill-opacity", 1);//使文字完全不透明。


                // 它为要被删除的节点添加过渡动画，然后将它们从 DOM 中移除
                var nodeExit = node.exit().transition()
                    .duration(duration)//过渡动画的持续时间
                    .attr("transform", function (d) {
                        return "translate(" + source.x + "," + source.y + ")";
                    })
                    .remove();
                // nodeExit.select("rect")
                //     .attr("width", rectW)
                //     .attr("height", rectH)
                //     //.attr("width", bbox.getBBox().width)""
                //     //.attr("height", bbox.getBBox().height)
                //     .attr("stroke", "black")
                //     .attr("stroke-width", 1);
                // nodeExit.select("text");


                // 更新路径
                var link = svg.selectAll("path.link")//选择现有的连线并绑定数据
                    .data(links, function (d) {
                        // console.log("🚀path.link ~ d:", d)
                        return d.target.id;
                    });
                // 当父节点展开与收起时更新新的路径
                let linkEnter = link.enter().insert("path", "g")
                    .attr("class", "link")
                    .attr("x", rectW / 2)
                    .attr("y", rectH / 2)
                    .attr("d", function (d) {
                        var o = {
                            x: source.x0,
                            y: source.y0
                        };
                        return linkVertical({
                            source: o,
                            target: o
                        });
                    });
                let linkUpdate = linkEnter.merge(link);//合并新旧数据元素
                console.log("🚀 ~ update ~ linkUpdate:", linkUpdate)
                linkUpdate.transition()//更新所有连线的位置
                    .duration(duration)
                    .attr("d", linkVertical);
                link.exit().transition()//移除不再需要的连线
                    .duration(duration)
                    .attr("d", function (d) {
                        var o = {
                            x: source.x,
                            y: source.y
                        };
                        return linkVertical({
                            source: o,
                            target: o
                        });
                    })
                    .remove();

                //  D3.js 中，树状图的更新动画是通过平滑地过渡节点和链接的位置来实现的。为了实现平滑过渡，需要在每次更新时记住节点的上一次位置。
                // 在下一次更新时，可以使用这些保存的位置来计算过渡动画的起始位置。
                nodes.forEach(function (d) {
                    d.x0 = d.x;
                    d.y0 = d.y;
                });
                // console.log("🚀 ~ nodes:", nodes)


                /*     if (source.id === getCount(root) + 1) {
                        console.log("触发");
                        //折叠区域渲染 使用延迟渲染优化显示效果
                        setTimeout(function () {
                            renderCollapseArea();
                        }, 200)
                    }
                    function renderCollapseArea() {
                        function handleClick(e) {
                            let target = e.target.dataset;
                            console.log("🚀 ~ handleClick ~ target:", target, "aaaaaaaaaaaaaaaaaaa")
                            let {
                                mclass,
                                type,
                                collapsed,
                                self
                            } = target;
                            if (type !== "btn") {
                                return
                            }
                            if (collapsed === "false") { //展开
                                d3.select(`.${mclass}`).transition().style("display", "none");
                                e.target.dataset.collapsed = "true";
                                document.getElementsByClassName(`${self}`)[0].innerHTML = "+"
                            } else {
                                d3.select(`.${mclass}`).transition().style("display", "block")
                                e.target.dataset.collapsed = "false";
                                document.getElementsByClassName(`${self}`)[0].innerHTML = "-"
                            }
                        }
                        window.handleClick = handleClick;
                    }
                */
            }

            //辅助函数: 递归获取tree的节点数量
            function getCount(parent) {
                var count = 0;
                if (Array.isArray(parent.children)) {
                    count += parent.children.length;
                    parent.children.forEach(function (child) {
                        if (Array.isArray(child.children)) {
                            count += getCount(child);
                        }
                    });
                }
                return count;
            }
            function getNode(d, callback) {
                //d是被点击的节点 将这个作为参数去获取不同的子节点
                //模拟接口
                setTimeout(() => {
                    let res = {
                        zhejiang: [{ data: { name: "杭州" } },
                        { data: { name: "宁波" } },
                        { data: { name: "温州" } }],
                        guangxi: [
                            { data: { name: "桂林" } },
                            { data: { name: "南宁" } },
                            { data: { name: "柳州" } },
                            { data: { name: "防城港" } }
                        ]
                    }
                    let _res = res[`${d.data._name}`];
                    _res && callback(_res);
                }, 100);
            }

            function click(d) {
                console.log("🚀 ~ click ~ click:")
                /*       if (d.id === getCount(root) + 1) {
                          //折叠前隐藏div区域
                          setTimeout(function () {
                              d3.selectAll("foreignObject").style("display", "none");
                          }, 200)
                      } */
                // debugger;
                // 展开  折叠节点
                if (d.children) {
                    console.log("🚀 ~ click ~ children:", "折叠")
                    d._children = d.children;
                    d.children = null;
                    update(d);
                } else if (d._children) {
                    console.log("🚀 ~ click ~ children:", "展开")
                    d.children = d._children;
                    d._children = null;
                    update(d);
                    // 异步加载子节点：
                } else if (!d.children && !d._children) {
                    console.log("🚀 ~ click ~ children:", "异步加载子节点")
                    let _y = (d.depth + 1) * 2;
                    getNode(d, function (children) {
                        children.forEach((node) => {
                            node.depth = d.depth + 1;
                            node.height = 0;
                            node.x = d.x;
                            node.x0 = d.x0;
                            node.y = _y;
                            node.y0 = _y;
                            node.parent = d;
                        })
                        d.children = children;
                        d._children = d.children;
                        console.log(d, "111111111111");
                        update(d);
                    })
                }
            }
            /*
            node.data：节点的数据。
            node.depth：节点的深度（根节点的深度为 0）。
            node.height：节点的高度（从节点到其最远后代节点的距离）。
            node.parent：节点的父节点。
            node.children：节点的子节点数组。
            node.x 和 node.y：节点的当前坐标。
            node.x0 和 node.y0：节点的前一次坐标，用于过渡效果。
            {
              source: NodeObject, // 父节点
              target: NodeObject  // 子节点
            }
            */



        }


        createTree();
        // const data = [10, 20, 30, 40, 50];

        // const width = 500;
        // const height = 500;
        // const barWidth = 40;

        // const svg = d3.select("body").append("svg")
        //     .attr("width", width)
        //     .attr("height", height);

        // svg.selectAll("rect")
        //     .data(data)
        //     .enter()
        //     .append("rect")
        //     .attr("x", (d, i) => i * (barWidth + 10))
        //     .attr("y", d => height - d * 10)
        //     .attr("width", barWidth)
        //     .attr("height", d => d * 10)
        //     .attr("fill", "teal");
    </script>
</body>

</html>